<?php

/**
 * Ignus Framework
 *
 * @author Sergey Palyanov <sergey.palyanov@gmail.com>
 * @link http://code.google.com/p/ignus-php/
 * @license http://apache.org/licenses/LICENSE-2.0
 * @package Ignus
 * @subpackage App
 * @since 0.1
 */

namespace Ignus;

/**
 * @package Ignus
 * @subpackage App
 * @since 0.1
 */
class Router
{
    /**
     * Route record indexes
     */
    const ROUTE_CONTROLLER = 0;
    const ROUTE_ACTION = 1;
    const ROUTE_PATH = 2;
    const ROUTE_ELEMENTS = 3;

    /**
     * @var array Registered routes
     */
    private $routes = array();

    /**
     * @var string Request URI path
     */
    private $route;

    /**
     * @var string Request method (POST, GET, etc)
     */
    private $method;

    /**
     * @var string Controller class name
     */
    private $controller = 'Application';

    /**
     * @var string Action method name
     */
    private $action = 'index';

    /**
     * @var array Request parameters
     */
    private $params;

    /**
     * @param string $route Request URI
     * @param string $method HTTP method
     */
    public function __construct($route, $method)
    {
        $this->route = $route;
        $this->method = Http::isAjax() ? 'AJAX' : $method;
        $this->params = $_GET + $_POST; // $_REQUEST has cookies occasionally
    }

    /**
     * Sets up request route
     * @return void
     */
    private function parseRoute()
    {
        if ($this->method != 'GET' && !isset($this->routes[$this->method]))
            throw new \RuntimeException("No routes for method '$this->method' defined");

        $parts = explode('/', trim($this->route, '/'));
        $count = count($parts);

        foreach ($this->routes[$this->method] as $route) {
            if ($count != $route[self::ROUTE_ELEMENTS])
                continue;

            $params = array();

            for ($i = 0; $i < $count; $i++) {
                $part = $route[self::ROUTE_PATH][$i];

                if (is_array($part)) {
                    $params[$part[0]] = $parts[$i];
                    continue;
                }

                if ($part != $parts[$i])
                    continue 2;
            }

            $this->controller = $route[self::ROUTE_CONTROLLER];
            $this->action = $route[self::ROUTE_ACTION];
            $this->params += $params;

            return;
        }

        // No configured route matched
        // Using path elements to determine controller and action

        if (count($parts) > 0)
            $this->controller = ucfirst(array_shift($parts));

        if (count($parts) > 0)
            $this->action = array_shift($parts);
    }

    /**
     * Starts request processing
     * @return Controller Controller object
     */
    public function run()
    {
        $this->parseRoute();
        $controller = $this->followRoute();
        $method = new \ReflectionMethod($controller, $this->action);

        $args = array();
        foreach ($method->getParameters() as $param) {
            $args[] = $this->getParamValue($param);
        }

        call_user_func_array(array($controller, $this->action), $args);
    }

    /**
     * Returns default method argument value
     * @param \ReflectionParameter $param
     * @return mixed Default method argument value or default value
     * @since 0.4
     */
    private function getParamValue(\ReflectionParameter $param)
    {
        $default = '';
        if ($param->isArray())
            $default = array();
        else if ($param->getClass())
            $default = null;

        $name = $param->getName();

        if (isset($this->params[$name])) {
            if ($param->isArray())
                return (array) $this->params[$name];
            return $this->params[$name];
        }

        return $param->isDefaultValueAvailable() ? $param->getDefaultValue() : $default;
    }

    /**
     * Parses route for the controller class
     * @return Controller Controller object
     */
    private function followRoute()
    {
        $controllerClass = 'Controllers\\' .  $this->controller;

        if (!class_exists($controllerClass, true))
            throw new \RuntimeException("Controller class '$controllerClass' not found");

        if (!method_exists($controllerClass, $this->action))
            throw new \RuntimeException("Method '$this->action' is not defined at '$controllerClass' class");

        return new $controllerClass($this, $this->params);
    }

    /**
     * Register route
     * @param string $method Request method
     * @param array $route Route
     * @return void
     */
    public function registerRoute($method, array $route)
    {
        $this->routes[$method][] = $route;
    }

    /**
     * Returns controller class name
     * @return string
     * @since 0.4
     */
    public function getController()
    {
        return $this->controller;
    }

    /**
     * Returns action method name
     * @return string
     * @since 0.4
     */
    public function getAction()
    {
        return $this->action;
    }

    /**
     * Make URI path for specified controller and action
     * @param string $controller Controller name
     * @param string $action Action name
     * @param array $params Parameters
     * @return string URI path
     * @since 0.4
     */
    public function assemble($controller, $action = 'index', array $params = array())
    {
        foreach ($this->routes as $routes) {
            foreach ($routes as $route) {
                if ($route[self::ROUTE_CONTROLLER] != $controller)
                    continue;
                if ($route[self::ROUTE_ACTION] != $action)
                    continue;

                $parts = array();
                foreach ($route[self::ROUTE_PATH] as $element) {
                    if (is_array($element)) {
                        list($param) = $element;
                        if (!isset($params[$param]))
                            throw new \RuntimeException("Missing required parameter '$param'");
                        $parts[] = $params[$param];
                        unset($params[$param]);
                    }
                    else {
                        $parts[] = $element;
                    }
                }

                $path = '/' . implode('/', $parts);
                return count($params)
                    ? $path . '?' . http_build_query($params)
                    : $path;
            }
        }

        // default assembling behaviour for GET requests only
        $path = '/' . strtolower($controller);

        // default action is optional
        if ($action != 'index')
            $path .= '/' . $action;

        return count($params)
            ? $path . '?' . http_build_query($params)
            : $path;
    }
}
